package aceim.protocol.snuk182.vkontakte.internal; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.zip.GZIPInputStream; import org.apache.http.Header; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.NameValuePair; import org.apache.http.client.ClientProtocolException; import org.apache.http.client.HttpClient; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpRequestBase; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.message.BasicNameValuePair; import org.apache.http.params.BasicHttpParams; import org.apache.http.params.CoreConnectionPNames; import org.apache.http.params.HttpConnectionParams; import org.apache.http.params.HttpParams; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import aceim.api.utils.Logger; import aceim.api.utils.Logger.LoggerLevel; import aceim.api.utils.Utils; import aceim.protocol.snuk182.vkontakte.VkConstants; import aceim.protocol.snuk182.vkontakte.VkEntityAdapter; import aceim.protocol.snuk182.vkontakte.model.AccessToken; import aceim.protocol.snuk182.vkontakte.model.ApiObject; import aceim.protocol.snuk182.vkontakte.model.LongPollResponse; import aceim.protocol.snuk182.vkontakte.model.LongPollResponse.LongPollResponseUpdate; import aceim.protocol.snuk182.vkontakte.model.LongPollServer; import aceim.protocol.snuk182.vkontakte.model.VkBuddy; import aceim.protocol.snuk182.vkontakte.model.VkBuddyGroup; import aceim.protocol.snuk182.vkontakte.model.VkChat; import aceim.protocol.snuk182.vkontakte.model.VkMessage; import aceim.protocol.snuk182.vkontakte.model.VkOnlineInfo; import android.net.Uri; import android.text.TextUtils; final class VkEngine { private static final String API_URL = "https://api.vk.com/method/"; private static final String TOKEN_URL = "https://oauth.vk.com/access_token"; private static final String PARAM_ACCESS_TOKEN = "access_token"; private final String accessToken; private final String internalUserId; private PollListenerThread listener; private final VkEngineConnector connector; VkEngine(String accessToken, String internalUserId) { this.accessToken = accessToken; this.internalUserId = internalUserId; this.connector = new VkEngineConnector(); } void sendTypingNotifications(String uid, boolean isChat) throws RequestFailedException { Map<String, String> params = new HashMap<String, String>(); params.put(isChat ? "chat_id" : "uid", uid); params.put("type", "typing"); doGetRequest("messages.setActivity", accessToken, params); } void setStatus(String statusString) throws RequestFailedException { Map<String, String> params = new HashMap<String, String>(); params.put("text", statusString); doGetRequest("status.set", accessToken, params); } void markMessagesAsRead(long[] messageIds) throws RequestFailedException { if (messageIds == null || messageIds.length < 1) { return; } StringBuilder sb = new StringBuilder(); for (int i=0; i<messageIds.length; i++) { sb.append(messageIds[i]); if (i < (messageIds.length - 1)) { sb.append(','); } } Map<String, String> params = new HashMap<String, String>(); params.put("mids", sb.toString()); doGetRequest("messages.markAsRead", accessToken, params); } List<VkChat> getGroupChats() throws RequestFailedException { Map<String, String> params = new HashMap<String, String>(); params.put("count", "200"); String result = doGetRequest("messages.getDialogs", accessToken, params); try { JSONArray array = new JSONObject(result).getJSONArray("response"); List<String> chats = new ArrayList<String>(); for (int i=0; i<array.length(); i++) { JSONObject jo = array.optJSONObject(i); String chatId = null; if (jo != null && !TextUtils.isEmpty((chatId = jo.optString("chat_id")))) { chats.add(chatId); } } List<VkChat> vkchats = new ArrayList<VkChat>(chats.size()); for (String chatId : chats) { params.clear(); params.put("chat_id", chatId); result = doGetRequest("messages.getChat", accessToken, params); try { VkChat chat = new VkChat(new JSONObject(result)); if (chat.getUsers().length > 2) { vkchats.add(chat); } } catch (JSONException e) { Logger.log(e); } } return vkchats; } catch (JSONException e) { throw new RequestFailedException(e); } } List<VkOnlineInfo> getOnlineBuddies() throws RequestFailedException { String result = doGetRequest("friends.getOnline", accessToken, null); return ApiObject.parseArray(result, VkOnlineInfo.class); } List<VkBuddyGroup> getBuddyGroupList() throws RequestFailedException { String result = doGetRequest("friends.getLists", accessToken, null); return ApiObject.parseArray(result, VkBuddyGroup.class); } List<VkBuddy> getBuddyList() throws RequestFailedException { Map<String, String> params = new HashMap<String, String>(); params.put("fields", "first_name,last_name,nickname,photo_big,lid"); String result = doGetRequest("friends.get", accessToken, params); return ApiObject.parseArray(result, VkBuddy.class); } Map<String, String> getPhotosById(String[] photoIds) throws RequestFailedException { //return getSomethingByIds(photoIds, "photos.getById", "photos", otherParams, null, null); StringBuilder sb = new StringBuilder(); for (int i=0; i<photoIds.length; i++) { sb.append(photoIds[i]); if (i < photoIds.length-1) { sb.append(","); } } Map<String, String> params = new HashMap<String, String>(); params.put("photos", sb.toString()); params.put("photo_sizes", "1"); String result = doGetRequest("photos.getById", accessToken, params); Map<String, String> photos = new HashMap<String, String>(); try { JSONArray array = new JSONObject(result).getJSONArray("response"); for (int i = 0; i < array.length(); i++) { JSONObject jo = array.optJSONObject(i); if (jo == null) { continue; } JSONArray sizes = jo.optJSONArray("sizes"); int index = 0; int topSize = 0; for (int j=0; j<sizes.length(); j++) { JSONObject sizeObject = sizes.getJSONObject(j); int size = sizeObject.getInt("width"); if (topSize < size) { topSize = size; index = j; } } String src = sizes.getJSONObject(index).getString("src"); photos.put(src, null); } } catch (JSONException e) { Logger.log(e); } return photos; } Map<String, String> getDocsById(String[] docIds) throws RequestFailedException { return getSomethingByIds(docIds, "docs.getById", "docs", null, "url", new String[]{"title"}); } Map<String, String> getVideosById(String[] videoIds) throws RequestFailedException { return getSomethingByIds(videoIds, "video.get", "videos", null, "player", new String[]{"title"}); } Map<String, String> getAudiosById(String[] audioIds) throws RequestFailedException { return getSomethingByIds(audioIds, "audio.getById", "audios", null, "url", new String[]{"artist", "title"}); } private Map<String, String> getSomethingByIds(String[] ids, String methodName, String paramName, Map<String, String> otherParams, String sourceParam, String[] titleParam) throws RequestFailedException { StringBuilder sb = new StringBuilder(); for (int i=0; i<ids.length; i++) { sb.append(ids[i]); if (i < ids.length-1) { sb.append(","); } } Map<String, String> params = new HashMap<String, String>(); params.put(paramName, sb.toString()); if (otherParams != null) { params.putAll(otherParams); } String result = doGetRequest(methodName, accessToken, params); Map<String, String> photos = new HashMap<String, String>(); try { JSONArray array = new JSONObject(result).getJSONArray("response"); for (int i = 0; i < array.length(); i++) { JSONObject jo = array.optJSONObject(i); if (jo == null) { continue; } String src = jo.optString(sourceParam); String title = null; if (titleParam != null && titleParam.length > 0) { StringBuilder bb = new StringBuilder(); for (int j=0; j<titleParam.length; j++) { bb.append(jo.optString(titleParam[j])); if (j < titleParam.length-1) { bb.append(" - "); } } title = bb.toString(); } photos.put(src, title); } } catch (JSONException e) { Logger.log(e); } return photos; } VkBuddy getMyInfo() throws RequestFailedException { Map<String, String> params = new HashMap<String, String>(); params.put("uids", internalUserId); params.put("fields", "first_name,last_name,nickname,photo_big"); String result = doGetRequest("users.get", accessToken, params); return ApiObject.parseArray(result, VkBuddy.class).get(0); } String requestStatus(long uid) throws RequestFailedException { Map<String, String> params = new HashMap<String, String>(); params.put("uid", Long.toString(uid)); String result = doGetRequest("status.get", accessToken, params); try { return Utils.unescapeXMLString(new JSONObject(result).getJSONObject("response").getString("text")); } catch (JSONException e) { Logger.log(e); throw new RequestFailedException(e); } } void connectLongPoll(int pollWaitTime, LongPollCallback callback) throws RequestFailedException { Logger.log("Get new longpoll server connection", LoggerLevel.VERBOSE); try { LongPollServer lpServer = getLongPollServer(); startLongPollConnection(pollWaitTime, lpServer, callback); } catch (JSONException e) { Logger.log(e); } } List<VkBuddy> getUsersByIdList(long[] users) throws RequestFailedException { if (users == null) return null; StringBuilder sb = new StringBuilder(); for (int i=0; i<users.length; i++) { long userId = users[i]; sb.append(userId); if (i < (users.length - 1)) { sb.append(","); } } Map<String, String> params = new HashMap<String, String>(); params.put("uids", sb.toString()); params.put("fields", "first_name,last_name,nickname,photo_big"); String result = doGetRequest("users.get", accessToken, params); return ApiObject.parseArray(result, VkBuddy.class); } VkChat getChatById(String chatId) throws RequestFailedException { Map<String, String> params = new HashMap<String, String>(); params.put("chat_id", chatId); String result = doGetRequest("messages.getChat", accessToken, params); return new VkChat(result); } List<VkMessage> getLastChatMessages(String id, boolean isChat) throws RequestFailedException { Map<String, String> params = new HashMap<String, String>(); params.put(isChat ? "chat_id" : "uid", id); params.put("count", "3"); params.put("rev", "1"); String result = doGetRequest("messages.getHistory", accessToken, params); return ApiObject.parseArray(result, VkMessage.class); } private LongPollServer getLongPollServer() throws JSONException, RequestFailedException { String result = doGetRequest("messages.getLongPollServer", accessToken, null); return new LongPollServer(result); } private String doGetRequest(String apiMethodName, String accessToken, Map<String, String> params) throws RequestFailedException { if (accessToken == null) { throw new IllegalStateException("Empty access token"); } if (params == null) { params = new HashMap<String, String>(1); } params.put(PARAM_ACCESS_TOKEN, accessToken); return connector.request(Method.GET, API_URL + apiMethodName, VkEntityAdapter.map2NameValuePairs(params), null, null); } private void startLongPollConnection(int pollWaitTime, LongPollServer lpServer, LongPollCallback callback) { if (listener != null && !listener.isInterrupted()) { listener.interrupt(); } listener = new PollListenerThread(pollWaitTime, lpServer, callback); listener.start(); } public long sendMessage(VkMessage message) throws RequestFailedException { String result = doGetRequest("messages.send", accessToken, message.toParamsMap()); try { return new JSONObject(result).getLong("response"); } catch (JSONException e) { Logger.log(e); return 0; } } public byte[] getIcon(String url) throws RequestFailedException { try { return connector.requestRawStream(Method.GET, url, null, null, null, null); } catch (Exception e) { disconnect(e.getLocalizedMessage()); throw new RequestFailedException(e); } } static AccessToken getAccessToken(String code) throws RequestFailedException { List<NameValuePair> params = new ArrayList<NameValuePair>(4); params.add(new BasicNameValuePair("client_id", VkApiConstants.API_ID)); params.add(new BasicNameValuePair("client_secret", VkApiConstants.API_SECRET)); params.add(new BasicNameValuePair("code", code)); params.add(new BasicNameValuePair("redirect_uri", VkConstants.OAUTH_REDIRECT_URL)); String json = new VkEngineConnector().request(Method.GET, TOKEN_URL, params, null, null); try { AccessToken token = new AccessToken(json); return token; } catch (JSONException e) { Logger.log(e); return null; } } private final class PollListenerThread extends Thread { private static final String LONGPOLL_MODE_GET_ATTACHMENTS = "2"; private final int pollWaitSeconds; private final HttpClient httpClient = new DefaultHttpClient(getHttpParams()); private final String url; private final String key; private String ts; private final LongPollCallback callback; private String lastUpdate = ""; private volatile boolean isClosed = false; private PollListenerThread(int pollWaitTime, LongPollServer lpServer, LongPollCallback callback) { this.url = lpServer.getServer(); this.key = lpServer.getKey(); this.ts = lpServer.getTs(); this.callback = callback; this.pollWaitSeconds = pollWaitTime > 0 ? pollWaitTime : Integer.parseInt(VkConstants.POLL_WAIT_TIME); } private void disconnect(String reason) { if (!isInterrupted() && !isClosed) { isClosed = true; interrupt(); httpClient.getConnectionManager().shutdown(); if (callback != null) { callback.disconnected(reason); } } } @Override public void run() { while (!isInterrupted() && !isClosed) { List<NameValuePair> params = new ArrayList<NameValuePair>(5); params.add(new BasicNameValuePair("act", "a_check")); params.add(new BasicNameValuePair("key", key)); params.add(new BasicNameValuePair("ts", ts)); params.add(new BasicNameValuePair("wait", Integer.toString(pollWaitSeconds))); params.add(new BasicNameValuePair("mode", LONGPOLL_MODE_GET_ATTACHMENTS)); try { String responseString = connector.request(Method.GET, "http://" + url, params, null, null, httpClient); LongPollResponse response = new LongPollResponse(responseString); ts = response.getTs(); String currentUpdate = response.getUpdatesJSON(); if (!lastUpdate.equals(currentUpdate)) { lastUpdate = currentUpdate; for (LongPollResponseUpdate u : response.getUpdates()) { processUpdate(u, callback); } } else { Logger.log(" ... repeated " + currentUpdate); Thread.sleep(1000); } if (response.isConnectionDead()) { Logger.log("Dead longpoll connection", LoggerLevel.VERBOSE); interrupt(); } } catch (Exception e) { Logger.log(e); disconnect(e.getLocalizedMessage()); } } if (!isClosed) { try { connectLongPoll(pollWaitSeconds, callback); } catch (RequestFailedException e) { Logger.log(e); } } } private void processUpdate(LongPollResponseUpdate update, LongPollCallback callback) { if (update == null) { return; } switch (update.getType()) { case BUDDY_ONLINE: case BUDDY_OFFLINE_AWAY: VkOnlineInfo vi = VkOnlineInfo.fromLongPollUpdate(update); callback.onlineInfo(vi); break; case MSG_NEW: VkMessage vkm = VkMessage.fromLongPollUpdate(update); callback.message(vkm); break; case BUDDY_TYPING: callback.typingNotification(update.getId(), 0); break; case BUDDY_TYPING_CHAT: callback.typingNotification(Long.parseLong(update.getParams()[0]), update.getId()); break; default: Logger.log("LongPoll update: " + update.getType() + "/" + update.getId() + "/" + update.getParams(), LoggerLevel.INFO); break; } } } public void disconnect(String reason) { if (listener != null) { listener.disconnect(reason); } } private static HttpParams getHttpParams() { HttpParams httpParameters = new BasicHttpParams(); int timeoutConnection = 120000; int timeoutSocket = 120000; HttpConnectionParams.setSoTimeout(httpParameters, timeoutSocket); HttpConnectionParams.setConnectionTimeout(httpParameters, timeoutConnection); httpParameters.setParameter("http.useragent", "Android mobile"); httpParameters.setParameter(CoreConnectionPNames.TCP_NODELAY, Boolean.FALSE); return httpParameters; } public static class RequestFailedException extends Exception { private static final long serialVersionUID = -7412616341196774522L; private RequestFailedException(Exception inner) { super(inner); } private RequestFailedException(String reason) { super(reason); } } private enum Method { POST, GET } public interface LongPollCallback { void onlineInfo(VkOnlineInfo vi); void message(VkMessage vkm); void typingNotification(long contactId, long chatParticipantId); void disconnected(String reason); } private static class VkEngineConnector { private String lastRequest = ""; private String request(Method method, String url, List<NameValuePair> parameters, String content, List<? extends NameValuePair> nameValuePairs) throws RequestFailedException { return request(method, url, parameters, content, nameValuePairs, null); } private String request(Method method, String url, List<NameValuePair> parameters, String content, List<? extends NameValuePair> nameValuePairs, HttpClient httpClient) throws RequestFailedException { try { byte[] resultBytes = requestRawStream(method, url, parameters, content, nameValuePairs, httpClient); String result = new String(resultBytes, "UTF-8"); Logger.log("Got " + result, LoggerLevel.VERBOSE); return result; } catch (Exception e) { throw new RequestFailedException(e); } } private byte[] requestRawStream(Method method, String url, List<NameValuePair> parameters, String content, List<? extends NameValuePair> nameValuePairs, HttpClient httpClient) throws RequestFailedException, URISyntaxException, ClientProtocolException, IOException { if (url.equals(lastRequest)) { try { Thread.sleep(400); } catch (InterruptedException e) {} } lastRequest = url; Uri.Builder b = new Uri.Builder(); b.encodedPath(url); if (parameters != null) { for (NameValuePair p : parameters) { b.appendQueryParameter(p.getName(), p.getValue()); } } url = b.build().toString(); HttpRequestBase request; switch (method) { case GET: request = new HttpGet(new URI(url)); break; case POST: request = new HttpPost(new URI(url)); if (nameValuePairs != null) { ((HttpPost) request).setEntity(new UrlEncodedFormEntity(nameValuePairs)); } if (content != null) { ((HttpPost) request).setEntity(new StringEntity(content)); } break; default: Logger.log("Unknown request method " + method, LoggerLevel.WTF); return null; } HttpEntity httpEntity = null; HttpClient innerHttpClient = httpClient; try { if (innerHttpClient == null){ innerHttpClient = new DefaultHttpClient(getHttpParams()); innerHttpClient.getParams().setParameter("http.useragent", "Android mobile"); } request.addHeader("Accept-Encoding", "gzip"); Logger.log("Ask " + url, LoggerLevel.VERBOSE); HttpResponse response = innerHttpClient.execute(request); Logger.log("..." + response.getStatusLine().getStatusCode(), LoggerLevel.VERBOSE); httpEntity = response.getEntity(); InputStream instream = httpEntity.getContent(); Header contentEncoding = response.getFirstHeader("Content-Encoding"); if (contentEncoding != null && contentEncoding.getValue().equalsIgnoreCase("gzip")) { instream = new GZIPInputStream(instream); } ByteArrayOutputStream ostream = new ByteArrayOutputStream(); byte[] buffer = new byte[8192]; int read = -1; while((read = instream.read(buffer, 0, buffer.length)) != -1) { ostream.write(buffer, 0, read); } ostream.flush(); return ostream.toByteArray(); } finally { if (httpEntity != null) { httpEntity.consumeContent(); } if (httpClient == null && innerHttpClient != null) { innerHttpClient.getConnectionManager().shutdown(); } } } } }